;;; magit-transient.el --- Support for transients  -*- lexical-binding:t -*-

;; Copyright (C) 2008-2025 The Magit Project Contributors

;; Author: Jonas Bernoulli <emacs.magit@jonas.bernoulli.dev>
;; Maintainer: Jonas Bernoulli <emacs.magit@jonas.bernoulli.dev>

;; SPDX-License-Identifier: GPL-3.0-or-later

;; Magit is free software: you can redistribute it and/or modify it
;; under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;;
;; Magit is distributed in the hope that it will be useful, but WITHOUT
;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
;; or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public
;; License for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with Magit.  If not, see <https://www.gnu.org/licenses/>.

;;; Commentary:

;; This library implements Magit-specific prefix and suffix classes,
;; and their methods.

;;; Code:

(require 'magit-git)
(require 'magit-mode)
(require 'magit-process)

(require 'transient)

;;; Classes

(defclass magit--git-variable (transient-variable)
  ((scope       :initarg :scope)
   (global      :initarg :global      :initform nil)
   (default     :initarg :default     :initform nil)))

(defclass magit--git-variable:choices (magit--git-variable)
  ((choices     :initarg :choices)
   (fallback    :initarg :fallback    :initform nil)))

(defclass magit--git-variable:boolean (magit--git-variable:choices)
  ((choices     :initarg :choices     :initform '("true" "false"))))

(defclass magit--git-variable:urls (magit--git-variable)
  ((seturl-arg  :initarg :seturl-arg  :initform nil)))

;;; Methods
;;;; Init

(cl-defmethod transient-init-scope ((obj magit--git-variable))
  (oset obj scope
        (cond (transient--prefix
               (oref transient--prefix scope))
              ((slot-boundp obj 'scope)
               (funcall (oref obj scope) obj)))))

(cl-defmethod transient-init-value ((obj magit--git-variable))
  (let ((variable (format (oref obj variable)
                          (oref obj scope)))
        (arg (if (oref obj global) "--global" "--local")))
    (oset obj variable variable)
    (oset obj value
          (cond ((oref obj multi-value)
                 (magit-get-all arg variable))
                ((magit-get arg variable))))))

(cl-defmethod transient-init-value ((obj magit--git-variable:boolean))
  (let ((variable (format (oref obj variable)
                          (oref obj scope)))
        (arg (if (oref obj global) "--global" "--local")))
    (oset obj variable variable)
    (oset obj value (if (magit-get-boolean arg variable) "true" "false"))))

;;;; Read

(cl-defmethod transient-infix-read :around ((obj magit--git-variable:urls))
  (transient--with-emergency-exit
    (transient--with-suspended-override
     (mapcar (lambda (url)
               (if (string-prefix-p "~" url)
                   (expand-file-name url)
                 url))
             (cl-call-next-method obj)))))

(cl-defmethod transient-infix-read ((obj magit--git-variable:choices))
  (let ((choices (oref obj choices)))
    (when (functionp choices)
      (setq choices (funcall choices)))
    (cond-let
      (current-prefix-arg
       (pcase-let*
           ((`(,fallback . ,choices)
             (magit--git-variable-list-choices obj))
            (choice (magit-completing-read
                     (format "Set `%s' to" (oref obj variable))
                     (if fallback (nconc choices (list fallback)) choices)
                     nil t)))
         (if (equal choice fallback) nil choice)))
      ([value (oref obj value)]
       (cadr (member value choices)))
      ((car choices)))))

;;;; Readers

(defun magit-transient-read-person (prompt initial-input history)
  (magit-completing-read
   prompt
   (mapcar (##save-excursion
             (and (string-match "\\`[\s\t]+[0-9]+\t" %)
                  (list (substring % (match-end 0)))))
           (magit-git-lines "shortlog" "-n" "-s" "-e" "HEAD"))
   nil nil initial-input history))

(defun magit-transient-read-revision (prompt initial-input history)
  (magit-completing-read prompt (cons "HEAD" (magit-list-refnames))
                         nil 'any initial-input history
                         (or (magit-branch-or-commit-at-point)
                             (magit-get-current-branch))))

;;;; Set

(cl-defmethod transient-infix-set ((obj magit--git-variable) value)
  (let ((variable (oref obj variable))
        (arg (if (oref obj global) "--global" "--local")))
    (oset obj value value)
    (if (oref obj multi-value)
        (magit-set-all value arg variable)
      (magit-set value arg variable))
    (magit-refresh)
    (unless (or value transient--prefix)
      (message "Unset %s" variable))))

(cl-defmethod transient-infix-set ((obj magit--git-variable:urls) values)
  (let ((previous (oref obj value))
        (seturl   (oref obj seturl-arg))
        (remote   (oref transient--prefix scope)))
    (oset obj value values)
    (dolist (v (cl-set-difference values previous :test #'equal))
      (magit-call-git "remote" "set-url" seturl "--add" remote v))
    (dolist (v (cl-set-difference previous values :test #'equal))
      (magit-call-git "remote" "set-url" seturl "--delete" remote
                      (concat "^" (regexp-quote v) "$")))
    (magit-refresh)))

;;;; Draw

(cl-defmethod transient-format-description ((obj magit--git-variable))
  (or (oref obj description)
      (oref obj variable)))

(cl-defmethod transient-format-value ((obj magit--git-variable))
  (cond-let*
    ([value (oref obj value)]
     (if (oref obj multi-value)
         (if (cdr value)
             (mapconcat (##concat "\n     "
                                  (propertize % 'face 'transient-value))
                        value "")
           (propertize (car value) 'face 'transient-value))
       (propertize (car (split-string value "\n"))
                   'face 'transient-value)))
    ([default (oref obj default)]
     [default (if (functionp default) (funcall default) default)]
     (concat (propertize "default:" 'face 'transient-inactive-value)
             (propertize default 'face 'transient-value)))
    ((propertize "unset" 'face 'transient-inactive-value))))

(cl-defmethod transient-format-value ((obj magit--git-variable:choices))
  (pcase-let ((`(,fallback . ,choices) (magit--git-variable-list-choices obj)))
    (concat
     (propertize "[" 'face 'transient-inactive-value)
     (mapconcat #'identity choices
                (propertize "|" 'face 'transient-inactive-value))
     (and fallback (propertize "|" 'face 'transient-inactive-value))
     fallback
     (propertize "]" 'face 'transient-inactive-value))))

(defun magit--git-variable-list-choices (obj)
  (let* ((variable (oref obj variable))
         (choices  (oref obj choices))
         (globalp  (oref obj global))
         (value    nil)
         (global   (magit-git-string "config" "--global" variable))
         (defaultp (oref obj default))
         (default  (if (functionp defaultp) (funcall defaultp obj) defaultp))
         (fallback (oref obj fallback))
         (fallback (and fallback
                        (and$ (magit-get fallback)
                              (concat fallback ":" $)))))
    (if (not globalp)
        (setq value (magit-git-string "config" "--local"  variable))
      (setq value global)
      (setq global nil))
    (when (functionp choices)
      (setq choices (funcall choices)))
    (cons (cond (global
                 (propertize (concat "global:" global)
                             'face (cond (value
                                          'transient-inactive-value)
                                         ((member global choices)
                                          'transient-value)
                                         ('font-lock-warning-face))))
                (fallback
                 (propertize fallback
                             'face (if value
                                       'transient-inactive-value
                                     'transient-value)))
                (default
                 (propertize (if (functionp defaultp)
                                 (concat "dwim:" default)
                               (concat "default:" default))
                             'face (if value
                                       'transient-inactive-value
                                     'transient-value))))
          (mapcar (lambda (choice)
                    (propertize choice 'face (if (equal choice value)
                                                 (if (member choice choices)
                                                     'transient-value
                                                   'font-lock-warning-face)
                                               'transient-inactive-value)))
                  (if (and value (not (member value choices)))
                      (cons value choices)
                    choices)))))

;;; _
(provide 'magit-transient)
;; Local Variables:
;; read-symbol-shorthands: (
;;   ("and$"         . "cond-let--and$")
;;   ("and>"         . "cond-let--and>")
;;   ("and-let"      . "cond-let--and-let")
;;   ("if-let"       . "cond-let--if-let")
;;   ("when-let"     . "cond-let--when-let")
;;   ("while-let"    . "cond-let--while-let")
;;   ("match-string" . "match-string")
;;   ("match-str"    . "match-string-no-properties"))
;; End:
;;; magit-transient.el ends here
